package com.epam.iunna.bookshop.controller.utils;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileFilter;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Iterator;
import java.util.Properties;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;

import org.w3c.dom.Attr;
import org.w3c.dom.DOMException;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;
/**
 * 
 * Предоставляет методы для работы с конфигом (Xml файлом).
 *
 */
public class XmlConfig /* implements ConfigInt */ {
/**
 * Педставляет полностю весь Xml файл конфига.
 * Хранит в себе более мелкии элементы (таким как nodes)
 */
    private Document doc = null;
    /**
     * Фабрика отстраивает обьекты XPath.
     */
    private XPathFactory factory = XPathFactory.newInstance();
    /**
     * Путь к файлу конфигурации.
     */
    public File source = null;
    /**
     * Папка с конфигом.
     */
    private String confPath = null;
    /**
     * Получаем DOM из файла, подключает импортируемые ветки.
     * @param sourceFile путь к файлу конфигурации
     */

    public XmlConfig(final File sourceFile) {
        try {
            source = sourceFile;
            System.out.println("create config " + sourceFile);
            confPath = sourceFile.getParent();
            DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
            DocumentBuilder builder = factory.newDocumentBuilder();
            doc = builder.parse(sourceFile);
            parse();
        } catch (ParserConfigurationException e) {
            e.printStackTrace();
        } catch (SAXException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
/**
 * Возвращает значение параметра.
 * @param paramName параметр
 * @return значение
 */
    public String getValue(final String paramName) {
        Node tmp = getNode(paramName);
        if (tmp != null) {
            if (tmp.getNodeType() == Node.ATTRIBUTE_NODE) {
                return tmp.getNodeValue();
            } else {
                return tmp.getTextContent();
            }
        }
        return null;
    }
/**
 * Возвращает значения всех параметров paramName.
 * @param paramName параметр
 * @return массив значений
 */
    public String[] getValues(final String paramName) {
        NodeList tmp = getNodes(paramName);
        String[] result = new String[tmp.getLength()];
        if (tmp != null) {
            for (int i = 0; i < tmp.getLength(); i++) {
                if (tmp.item(i) != null) {
                    if (tmp.item(i).getNodeType() == Node.ATTRIBUTE_NODE) {
                        result[i] = tmp.item(i).getNodeValue();
                    } else {
                        result[i] = tmp.item(i).getTextContent();
                    }
                }
            }
        }
        return result;
    }
/**
 * Взвращает названия атрибута и значений в обьекте свойств указаного параметра.
 * @param paramName параметр
 * @return Properties
 */
    public Properties getOnceProperties(final String paramName) {
        Node tmp = getNode(paramName);
        Properties result = new Properties();
        if (tmp != null) {
            NamedNodeMap attrMap = tmp.getAttributes();
            for (int j = 0; j < attrMap.getLength(); j++) {
                result.setProperty(attrMap.item(j).getNodeName(), attrMap.item(
                        j).getNodeValue());
            }
            result.setProperty("$text", tmp.getTextContent());
            return result;
        }
        return null;
    }
/**
 * Взвращает все параметры paramName как Properties. 
 * @param paramName параметр
 * @return Properties[]
 */
    public Properties[] getProperties(final String paramName) {
        NodeList tmp = getNodes(paramName);
        Properties[] result = new Properties[tmp.getLength()];
        if (tmp != null) {
            for (int i = 0; i < tmp.getLength(); i++) {
                result[i] = new Properties();
                NamedNodeMap attrMap = tmp.item(i).getAttributes();
                for (int j = 0; j < attrMap.getLength(); j++) {
                    result[i].setProperty(attrMap.item(j).getNodeName(),
                            attrMap.item(j).getNodeValue());
                }
                result[i].setProperty("$text", tmp.item(i).getTextContent());
            }
        }
        return result;
    }
/**
 * Возвращает нод с указаним именем(параметром).
 * @param paramName параметр
 * @return нод с указаним именем(параметром)
 */
    public Node getNode(final String paramName) {
        XPath xPath = factory.newXPath();
        try {
            Node node = (Node)xPath.evaluate(paramName, doc, XPathConstants.NODE);
            return node;
        } catch (XPathExpressionException e) {
            e.printStackTrace();
        }
        return null;
    }
/**
 * Возвращает список нодов с указаним именем(параметром).
 * @param paramName параметр
 * @return список нодов с указаним именем(параметром)
 */
    public NodeList getNodes(final String paramName) {
        XPath xPath = factory.newXPath();
        try {
            NodeList nodes = (NodeList)xPath.evaluate(paramName, doc, XPathConstants.NODESET);
            return nodes;
        } catch (XPathExpressionException e) {
            e.printStackTrace();
        }
        return null;
    }
/**
 * Сохраняет нод в filename.
 * @param xml что сохранять
 * @param filename куда сохранять
 */
    private void saveXml(final Node xml, final String filename) {
        TransformerFactory tFactory = TransformerFactory.newInstance();
        Transformer transformer;
        try {
            transformer = tFactory.newTransformer();
            DOMSource source = new DOMSource(xml);
            StreamResult result = new StreamResult(this.source);
            transformer.transform(source, result);
        } catch (TransformerConfigurationException e) {
            e.printStackTrace();
        } catch (TransformerException e) {
            e.printStackTrace();
        }
    }
/**
 * Сохраняет Xml вместе с импортируемыми ветками.
 */
    public void save() {
        NodeList incFiles = getNodes("//import[@src]");
        for (int i = 0; i < incFiles.getLength(); i++) {
            Element curr = (Element)incFiles.item(i);
            String filename = curr.getAttribute("src");
            if (filename.indexOf(".") == 0) {
                filename = confPath + filename.substring(1);
            }
            saveXml(curr.getFirstChild(), filename);
            curr.removeChild(curr.getFirstChild());
        }

        NodeList incDirs = getNodes("//import[@dir]/file");
        for (int i = 0; i < incDirs.getLength(); i++) {
            Element curr = (Element)incDirs.item(i);
            saveXml(curr.getFirstChild(), curr.getAttribute("src"));
            curr.getParentNode().removeChild(curr);
        }

        saveXml(doc, source.toString());
        parse();
    }
/**
 * Устанавливает значение параметра.
 * @param paramName параметр
 * @param value значение
 */
    public void setValue(final String paramName, final String value) {
        Node tmp = getNode(paramName);
        if (tmp != null) {
            if (tmp instanceof Attr) {
                tmp.setNodeValue(value);
            } else {
                NodeList childList = tmp.getChildNodes();
                for (int i = 0; i < childList.getLength(); i++) {
                    if (childList.item(i).getNodeType() != Node.TEXT_NODE) {
                        throw new DOMException((short)0, "Cannot set value for " + paramName);
                    }
                }
                tmp.setTextContent(value);
            }
        } else {
            throw new DOMException((short)0, "Node " + paramName + " not found");
        }
    }
/**
 * Создает значение для указаного в path параметра.
 * @param path путь к параметру
 * @param value значение
 */
    public void createValue(final String path, final Properties value) {
        String[] pathParts = path.split("/");
        int i = pathParts.length - 2;
        Node tmp = null;

        for (i = pathParts.length - 2; i >= 0; i--) {
            String p = StringUtils.join(pathParts, 0, i, "/");
            tmp = getNode(p);
            if (tmp != null) {
                break;
            }
        }

        if (tmp == null) {
            throw new DOMException((short)0, "Cannot create " + path);
        }

        for (int j = i + 1; j < pathParts.length; j++) {
            if (pathParts[j].indexOf("@") > -1
                    || pathParts[j].indexOf("[") > -1
                    || pathParts[j].indexOf("]") > -1) {
                throw new DOMException((short)0, "Cannot create " + path);
            }
        }

        for (int j = i + 1; j < pathParts.length; j++) {
            Node newNode = doc.createElement(pathParts[j]);
            tmp.appendChild(newNode);
            tmp = newNode;
        }

        Iterator keys = value.keySet().iterator();
        while (keys.hasNext()) {
            String key = (String)keys.next();
            ((Element)tmp).setAttribute(key, value.getProperty(key));
        }
    }
/**
 * Получает из Xml DOM.
 * @param sourceFile файл источник
 * @return  Document(разобранный XML)
 */
    private Document loadXml(final File sourceFile) {
        System.out.println("Load " + sourceFile);
        DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
        DocumentBuilder builder;
        try {
            builder = factory.newDocumentBuilder();
            return builder.parse(sourceFile);
        } catch (ParserConfigurationException e) {
            e.printStackTrace();
        } catch (SAXException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }
/**
 * Подключает ветки из src и dir к главному дереву.
 */
    private void parse() {
        NodeList incFiles = getNodes("//import[@src]");
        for (int i = 0; i < incFiles.getLength(); i++) {
            Element curr = (Element)incFiles.item(i);
            String filename = curr.getAttribute("src");
            if (filename.indexOf(".") == 0) {
                filename = confPath + filename.substring(1);
            }
            File f = new File(filename);
            try {
                f = f.getCanonicalFile();
                if (f.exists() && f.canRead()) {
                    curr.appendChild(doc.importNode(loadXml(f).getDocumentElement(), true));
                } else {
                    System.out.println("Cannot attach config " + f);
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        NodeList incDirs = getNodes("//import[@dir]");
        for (int i = 0; i < incDirs.getLength(); i++) {
            parseExternalDir((Element)incDirs.item(i));
        }
    }
/**
 * Подключает ветки из внешних файлов в указанной папке.
 * @param dirNode путь к папке.
 */
    private void parseExternalDir(final Element dirNode) {
        String path = dirNode.getAttribute("dir");
        if (path.indexOf(".") == 0) {
            path = confPath + path.substring(1);
        }
        File dir = new File(path);
        try {
            dir = dir.getCanonicalFile();
            if (dir.exists() && dir.canRead() && dir.isDirectory()) {
                File[] list = dir.listFiles(new FileFilter() {
                    public boolean accept(final File pathname) {
                        return pathname.toString().indexOf(".xml") > -1;
                    }
                });
                for (int i = 0; i < list.length; i++) {
                    Element f = doc.createElement("file");
                    f.setAttribute("src", list[i].toString());
                    dirNode.appendChild(f);
                    f.appendChild(doc.importNode(loadXml(list[i]).getDocumentElement(), true));
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
/**
 * Выводит в поток вывода весь файл.
 * @return файл конфигурации.
 */
    public String toString() {
        TransformerFactory tFactory = TransformerFactory.newInstance();
        Transformer transformer;
        try {
            transformer = tFactory.newTransformer();
            DOMSource source = new DOMSource(doc);
            OutputStream out = new ByteArrayOutputStream();
            StreamResult result = new StreamResult(out);
            transformer.transform(source, result);
            String res = out.toString();
            out.close();
            return res;
        } catch (TransformerConfigurationException e) {
            e.printStackTrace();
        } catch (TransformerException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return "";
    }

}
